package com.douyin.service.impl;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.douyin.config.LocalCache;
import com.douyin.config.QiNiuConfig;
import com.douyin.constant.AuditStatus;
import com.douyin.constant.RedisConstant;
import com.douyin.entity.FavoritesVideo;
import com.douyin.entity.File;
import com.douyin.entity.User;
import com.douyin.entity.task.VideoTask;
import com.douyin.entity.video.Type;
import com.douyin.entity.video.Video;
import com.douyin.entity.video.VideoShare;
import com.douyin.entity.video.VideoStar;
import com.douyin.exception.BaseException;
import com.douyin.mapper.VideoMapper;
import com.douyin.mapper.VideoShareMapper;
import com.douyin.service.*;
import com.douyin.service.audit.VideoPublishAuditServiceImpl;
import com.douyin.utils.FileUtil;
import com.douyin.utils.RedisCacheUtil;
import com.douyin.vo.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;

import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class VideoServiceImpl extends ServiceImpl<VideoMapper, Video> implements VideoService {

    @Autowired
    private RedisCacheUtil redisCacheUtil;

    @Autowired
    private FileService fileService;

    @Autowired
    private VideoPublishAuditServiceImpl videoPublishAuditService;

    @Autowired
    private IUserService userService;

    @Autowired
    private InterestPushService interestPushService;

    @Autowired
    private IFavoritesService favoritesService;

    @Autowired
    private VideoStarService videoStarService;

    @Autowired
    private VideoShareService videoShareService;

    @Autowired
    private TypeService typeService;
    @Autowired
    private FavoritesVideoService favoritesVideoService;

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IFollowService followService;
    @Autowired
    private FeedService feedService;
    @Autowired
    private VideoShareMapper videoShareMapper;

    final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void pushVideo(Video video,Long userId) {
        Long videoId = video.getId();
        Video oldVideo = null;
        if (videoId != null){
            //url不一致
            oldVideo = this.getOne( new LambdaQueryWrapper<Video>().eq(Video::getId,videoId).eq(Video::getUserId,userId));
            if (!video.buildVideoUrl().equals(oldVideo.buildVideoUrl())|| !video.buildCoverUrl().equals(oldVideo.buildCoverUrl())){
                throw  new BaseException("不能更换视频源，只能修改视频信息");
            }
        }

        //判断视频分类是否存在
        Long typeId = video.getTypeId();
        if (typeId ==null){
            throw  new BaseException("视频分类不存在");
        }

        //校验标签最多不能超过5个
        if (video.buildLabel().size()>5){
            throw  new BaseException("标签最多不能超过5个");
        }

        //修改状态
        video.setAuditStatus(AuditStatus.PROCESS);
        video.setUserId(userId);
        video.setGmtCreated(new Date());
        video.setGmtUpdated(new Date());
        boolean isAdd = videoId == null ;

        video.setYv(null);
        if (isAdd){
            //没有封面根据视频生产一个封面
            if (ObjectUtils.isEmpty(video.getCover())){
                video.setCover(fileService.generatePhoto(video.getUrl(),userId));
                //用于精确搜索
                video.setYv("YV"+ UUID.randomUUID().toString().replace("-","").substring(8));
            }
        }else {
            //不能修改的属性直接传null
            video.setVideoType(null);
            video.setLabelNames(null);
            video.setUrl(null);
            video.setCover(null);
        }

        // 填充视频时长 (若上次发布视频不存在Duration则会尝试获取)
        if (isAdd || !StringUtils.hasLength(oldVideo.getDuration())) {
            final String uuid = UUID.randomUUID().toString();
            LocalCache.put(uuid, true);
            try {
                Long url = video.getUrl();
                if (url == null || url == 0) url = oldVideo.getUrl();
                final String fileKey = fileService.getById(url).getFileKey();
                final String duration = FileUtil.getVideoDuration(QiNiuConfig.CNAME + "/" + fileKey + "?uuid=" + uuid);
                video.setDuration(duration);
            } finally {
                LocalCache.rem(uuid);
            }
        }

        this.saveOrUpdate(video);

        final VideoTask videoTask = new VideoTask();
        videoTask.setOldVideo(video);
        videoTask.setVideo(video);
        videoTask.setIsAdd(isAdd);
        videoTask.setOldState(isAdd ? true : video.getOpen());
        videoTask.setNewState(true);
        videoPublishAuditService.audit(videoTask, false);
    }

    @Override
    public Collection<Video> pushVideos(Long userId) {
        User user = new User();
        if (userId != null){
            user = userService.getById(userId);
        }
        Collection<Long> videoIds = interestPushService.listVideoIdByUserModel(user);
        Collection<Video> videoCollections =  new ArrayList<>();

        if (videoIds.isEmpty()){
            videoIds = list(new LambdaQueryWrapper<Video>().orderByDesc(Video::getGmtCreated)).stream().map(Video::getId).collect(Collectors.toList());
            videoIds = videoIds.stream().limit(10).collect(Collectors.toList());
        }
        videoCollections = listByIds(videoIds);

        setUserVoAndUrl(videoCollections);
        return  videoCollections;
    }

    @Override
    public boolean favorites(Long fId, Long vId, Long userId) {
        Video video = getById(vId);
        if (video == null){
            throw  new BaseException("视频不存在");
        }
        boolean favorite = favoritesService.favorites(fId,vId,userId);
        Long value = favorite ? 1L : -1L;
        final UpdateWrapper<Video> videoUpdateWrapper = new UpdateWrapper<>();
        videoUpdateWrapper.setSql("favorites_count = favorites_count + " + value);
        //加上乐观锁
        videoUpdateWrapper.lambda().eq(Video::getId,vId).eq(Video::getFavoritesCount,video.getFavoritesCount());
        update(video,videoUpdateWrapper);

        //更新用户兴趣模型
        final List<String> list = video.buildLabel();
        //收藏加2.0
        UserModel userModel = UserModel.buildUserModel(list,vId,2.0);
        interestPushService.updateUserModel(userModel);
        return favorite;
    }

    @Override
    public List<Video> listVideoByFavoritesId(Long favoritesId,Long userId) {
        List<Long> videoIds = favoritesService.listVideoIdByFavoritesId(favoritesId,userId);
        if (ObjectUtils.isEmpty(videoIds)){
            return Collections.emptyList();
        }
        List<Video> videos = listByIds(videoIds);
        setUserVoAndUrl(videos);
        return videos;
    }

    @Override
    public boolean starVideo(Long videoId, Long userId) {
        int maxRetries = 5; // 设置最大重试次数
        int retryCount = 0;

        while (retryCount < maxRetries) {
            Video video = getById(videoId);
            if (video == null) {
                throw new BaseException("视频不存在");
            }

            VideoStar videoStar = new VideoStar();
            videoStar.setVideoId(videoId);
            videoStar.setUserId(userId);
            boolean star = videoStarService.starVideo(videoStar);
            Long value = star ? 1L : -1L;

            final UpdateWrapper<Video> updateWrapper = new UpdateWrapper<>();
            updateWrapper.setSql("start_count = start_count + " + value);
            // 乐观锁
            updateWrapper.lambda().eq(Video::getId, video.getId()).eq(Video::getStartCount, video.getStartCount());

            boolean updateResult = update(video, updateWrapper);

            if (updateResult) {
                if (star) { // 更新用户兴趣模型
                    final List<String> list = video.buildLabel();
                    // 点赞加1.0
                    UserModel userModel = UserModel.buildUserModel(list, videoId, 1.0);
                    interestPushService.updateUserModel(userModel);
                }
                return star;
            } else {
                // 更新失败，可能是并发导致的乐观锁冲突，增加重试次数
                retryCount++;
                if (retryCount >= maxRetries) {
                    throw new BaseException("更新失败，重试次数达到上限");
                }
            }
        }

        // 如果达到最大重试次数仍未成功，抛出异常
        throw new BaseException("更新失败，重试次数达到上限");
    }

    @Override
    public boolean shareVideo(VideoShare videoShare) {
        Video video = getById(videoShare.getVideoId());
        if (video == null) {
            throw new BaseException("视频不存在");
        }

        final boolean result = videoShareService.share(videoShare);
        updateShare(video, result ? 1L : 0L);
        return result;
    }

    @Override
    public Collection<Video> getVideoByTypeId(Long typeId) {
        if (typeId == null) return Collections.EMPTY_LIST;
        final Type type = typeService.getById(typeId);
        if (type == null) return Collections.EMPTY_LIST;

        Collection<Long> videoIds = interestPushService.listVideoIdByTypeId(typeId);
        if (ObjectUtils.isEmpty(videoIds)) {
            return Collections.EMPTY_LIST;
        }
        final Collection<Video> videos = listByIds(videoIds);

        setUserVoAndUrl(videos);
        return videos;
    }

    @Override
    public void historyVideo(Long videoId, Long userId) {
        String key = RedisConstant.HISTORY_VIDEO + videoId + ":" + userId;
        final Object o = redisCacheUtil.get(key);
        if (o == null) {
            //保存三天
            redisCacheUtil.set(key, videoId, RedisConstant.HISTORY_TIME);
            final Video video = getById(videoId);
            video.setUser(userService.getUserInfo(userId.toString()));
            video.setTypeName(typeService.getById(video.getTypeId()).getName());
            //需要时间排序所以要用zSet
            redisCacheUtil.zadd(RedisConstant.USER_HISTORY_VIDEO + userId, new Date().getTime(), video, RedisConstant.HISTORY_TIME);
            //添加播放量
            Long value = 1L;
            final UpdateWrapper<Video> updateWrapper = new UpdateWrapper<>();
            updateWrapper.setSql("history_count = history_count + " + value);
            updateWrapper.lambda().eq(Video::getId, video.getId()).eq(Video::getHistoryCount, video.getHistoryCount());
            update(video, updateWrapper);
        }
    }

    @Override
    // LinkedHashMap 中的 key-value 是有序的
    public LinkedHashMap<String,List<Video>> getHistory(BasePage basePage, Long userId) {
        //需要观看时间排序从新到旧
        String key = RedisConstant.USER_HISTORY_VIDEO + userId;
        Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisCacheUtil.zSetGetByPage(key, basePage.getPage(), basePage.getLimit());
        if (ObjectUtils.isEmpty(typedTuples)) {
            return new LinkedHashMap<>();
        }
        List<Video> temp = new ArrayList<>();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        LinkedHashMap<String, List<Video>> result = new LinkedHashMap<>();
        for (ZSetOperations.TypedTuple<Object> typedTuple : typedTuples) {
            Date date = new Date(typedTuple.getScore().longValue());
            String format = simpleDateFormat.format(date);
            if (!result.containsKey(format)) {
                result.put(format, new ArrayList<>());
            }
            Video video = (Video) typedTuple.getValue();
            result.get(format).add(video);
            temp.add(video);
        }
        setUserVoAndUrl(temp);

        return result;
    }

    @Override
    public IPage<Video> listByUserIdVideo(BasePage basePage, Long userId) {
        final IPage page = page(basePage.page(), new LambdaQueryWrapper<Video>().eq(Video::getUserId, userId).orderByDesc(Video::getGmtCreated));
        setUserVoAndUrl(page.getRecords());
        return page;
    }

    @Override
    @Transactional
    public void deleteVideo(Long id,Long userId) {
        if (id== null){
            throw new BaseException("id不能为空");
        }
        Video video = getOne(new LambdaQueryWrapper<Video>().eq(Video::getId, id).eq(Video::getUserId, userId));
        if (video == null){
            throw new BaseException("视频不存在");
        }
        boolean result = removeById(id);
        if (result){
            // 删除分享量 点赞量
            videoShareService.remove(new LambdaQueryWrapper<VideoShare>().eq(VideoShare::getVideoId, id).eq(VideoShare::getUserId, userId));
            videoStarService.remove(new LambdaQueryWrapper<VideoStar>().eq(VideoStar::getVideoId, id).eq(VideoStar::getUserId, userId));
            interestPushService.deleteSystemStockIn(video);
            interestPushService.deleteSystemTypeStockIn(video);
        }
    }

    @Override
    public String getAuditQueueState() {
        return videoPublishAuditService.getAuditQueueState() ? "快速" : "慢速";
    }

    @Override
    public IPage<Video> searchVideo(String search, BasePage basePage, Long userId) {
        if (ObjectUtils.isEmpty(search)) {
            return null;
        }
        IPage iPage = basePage.page();
        // 如果带YV则精准搜该视频
        LambdaQueryWrapper<Video> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Video::getAuditStatus, AuditStatus.SUCCESS);
        if (search.contains("YV")) {
            wrapper.eq(Video::getYv, search);
        } else {
            wrapper.like(true, Video::getTitle, search);
        }
        IPage<Video> page = this.page(iPage, wrapper);
        final List<Video> videos = page.getRecords();
        setUserVoAndUrl(videos);

        userService.addSearchHistory(userId, search);
        return page;
    }

    @Override
    public IPage<Video> getVideoByUserId(Long userId, BasePage basePage) {
        IPage iPage = basePage.page();
        if (userId == null){
            return new Page<>();
        }
        IPage<Video> page = this.page(iPage,new LambdaQueryWrapper<Video>()
                .eq(Video::getUserId,userId).eq(Video::getAuditStatus,AuditStatus.SUCCESS)
                .orderByDesc(Video::getGmtCreated));

        setUserVoAndUrl(page.getRecords());
        return page;
    }

    //获取n天前的视频
    @Override
    public List<Video> selectNDaysAgeVideo(long id, int days, int limit) {
        return videoMapper.selectNDaysAgeVideo(id, days, limit);
    }

    @Override
    public List<HotVideo> getHotRank() {
        Set<ZSetOperations.TypedTuple<String>> hotRank = stringRedisTemplate.opsForZSet().reverseRangeWithScores(RedisConstant.HOT_RANK, 0, -1);
        ArrayList<HotVideo> hotVideos =new ArrayList<>();
        for (ZSetOperations.TypedTuple<String> typedTuple : hotRank) {
            HotVideo hotVideo = null;
            try {
                //将字符串转换为对象
                hotVideo = objectMapper.readValue(typedTuple.getValue(), HotVideo.class);
                hotVideo.setHot((double) typedTuple.getScore().intValue());
                hotVideo.hotFormat();
                hotVideos.add(hotVideo);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return hotVideos;
    }

    @Override
    public Collection<Video> getHotVideo() {
        Calendar calendar = Calendar.getInstance();
        int today = calendar.get(Calendar.DATE);

        final HashMap<String, Integer> map = new HashMap<>();
        // 优先推送今日的
        map.put(RedisConstant.HOT_VIDEO + today, 10);
        map.put(RedisConstant.HOT_VIDEO + (today - 1), 3);
        map.put(RedisConstant.HOT_VIDEO + (today - 2), 2);

        //获取hotVideo
        List<Long> hotVideoIds = redisCacheUtil.pipeline(connection -> {
            map.forEach((k, v) -> {
                connection.sRandMember(k.getBytes(), v);
            });
            return null;
        });
        if (ObjectUtils.isEmpty(hotVideoIds)) {
            return Collections.EMPTY_LIST;
        }

        ArrayList<Long> videoIds = new ArrayList<>();
        // 会返回结果有null，做下校验
        for (Object videoId : hotVideoIds) {
            if (!ObjectUtils.isEmpty(videoId)) {
                videoIds.addAll((List) videoId);
            }
        }
        if (ObjectUtils.isEmpty(videoIds)){
            return Collections.EMPTY_LIST;

        }
        Collection<Video> videos = listByIds(videoIds);
        // 和浏览记录做交集? 不需要做交集，热门视频和兴趣推送不一样
        setUserVoAndUrl(videos);
        return videos;
    }

    @Override
    public Collection<Video> listSimilarVideo(Video video) {
        if (ObjectUtils.isEmpty(video)|| ObjectUtils.isEmpty(video.getLabelNames()))return Collections.EMPTY_LIST;
        List<String> labels = video.buildLabel();
        Set<Long> videoIds = (Set<Long>) interestPushService.listVideoIdByLabels(labels);
        if (ObjectUtils.isEmpty(videoIds)){
            return Collections.EMPTY_LIST;
        }
        //去重自己
        videoIds.remove(video.getId());
        Collection<Video> videos = listByIds(videoIds);
        setUserVoAndUrl(videos);
        return  videos;
    }

    @Override
    public List<Long> listVideoIdByUserId(Long followId) {
        final List<Video> list = list(new LambdaQueryWrapper<Video>().eq(Video::getUserId, followId));
        return list.stream().map(Video::getId).collect(Collectors.toList());
    }

    @Override
    public Collection<Video> followFeed(Long userId, Long lastTime) {
        // 是否存在
        Set<Long> set = redisTemplate.opsForZSet().reverseRangeByScore(RedisConstant.IN_FOLLOW + userId,
                        0, lastTime == null ? new Date().getTime() : lastTime, lastTime == null ? 0 : 1, 5);
        if (ObjectUtils.isEmpty(set)) {
            // 可能只是缓存中没有了,缓存只存储7天内的关注视频,继续往后查看关注的用户太少了,不做考虑 - feed流必然会产生的问题
            return Collections.EMPTY_LIST;
        }

        // 这里不会按照时间排序，需要手动排序
        final Collection<Video> videos = list(new LambdaQueryWrapper<Video>().in(Video::getId, set).orderByDesc(Video::getGmtCreated));

        setUserVoAndUrl(videos);
        return videos;
    }

    @Override
    public void updateFollowFeed(Long userId) {
        List<Long> follows = followService.getFollow(userId,null);
        feedService.updateFollowFeed(userId,follows);
    }

    @Override
    public void auditProcess(Video video) {
        // 放行后
        updateById(video);
        interestPushService.pushSystemStockIn(video);
        interestPushService.pushSystemTypeStockIn(video);
        // 推送该视频博主的发件箱
        feedService.pushInBoxFeed(video.getUserId(), video.getId(), video.getGmtCreated().getTime());
    }

    @Override
    public void violations(Long id) {
        final Video video = getById(id);
        final Type type = typeService.getById(video.getTypeId());
        video.setLabelNames(type.getLabelNames());
        // 修改视频信息
        video.setOpen(true);
        video.setMsg("该视频违反了幸运日平台的规则,已被下架私密");
        video.setAuditStatus(AuditStatus.PASS);
        // 删除分类中的视频
        interestPushService.deleteSystemTypeStockIn(video);
        // 删除标签中的视频
        interestPushService.deleteSystemStockIn(video);
        // 获取视频发布者id,删除对应的发件箱
        final Long userId = video.getUserId();
        redisTemplate.opsForZSet().remove(RedisConstant.OUT_FOLLOW + userId, id);

        // 获取视频发布者粉丝，删除对应的收件箱
        final Collection<Long> fansIds = followService.getFans(userId, null);
        feedService.deleteInBoxFeed(userId, Collections.singletonList(id));
        feedService.deleteOutBoxFeed(userId, fansIds, id);

        // 热门视频以及热度排行榜视频
        Calendar calendar = Calendar.getInstance();
        int today = calendar.get(Calendar.DATE);
        final Long videoId = video.getId();
        // 尝试去找到删除
        redisTemplate.opsForSet().remove(RedisConstant.HOT_VIDEO + today, videoId);
        redisTemplate.opsForSet().remove(RedisConstant.HOT_VIDEO + (today - 1), videoId);
        redisTemplate.opsForSet().remove(RedisConstant.HOT_VIDEO + (today - 2), videoId);
        redisTemplate.opsForZSet().remove(RedisConstant.HOT_RANK, videoId);
        // 修改视频
        updateById(video);
    }

    public void updateShare(Video video, Long value) {
        final UpdateWrapper<Video> updateWrapper = new UpdateWrapper<>();
        updateWrapper.setSql("share_count = share_count + " + value);
        updateWrapper.lambda().eq(Video::getId, video.getId()).eq(Video::getShareCount, video.getShareCount());
        update(video, updateWrapper);
    }

    public void setUserVoAndUrl(Collection<Video> videos) {
        if (!ObjectUtils.isEmpty(videos)) {
            Set<Long> userIds = new HashSet<>();
            final ArrayList<Long> fileIds = new ArrayList<>();
            for (Video video : videos) {
                userIds.add(video.getUserId());
                fileIds.add(video.getUrl());
                fileIds.add(video.getCover());
            }
            final Map<Long, File> fileMap = fileService.listByIds(fileIds).stream().collect(Collectors.toMap(File::getId, Function.identity()));
            final Map<Long, User> userMap = userService.listByIds(userIds).stream().collect(Collectors.toMap(User::getId, Function.identity()));
            for (Video video : videos) {
                final UserVO userVO = new UserVO();
                final User user = userMap.get(video.getUserId());
                userVO.setId(video.getUserId());
                userVO.setNickName(user.getNickName());
                userVO.setDescription(user.getDescription());
                userVO.setSex(user.getSex());
                video.setUser(userVO);
                final File file = fileMap.get(video.getUrl());
                video.setVideoType(file.getFormat());
            }
        }
    }
}
